這篇程式碼在 https://github.com/DanSnow/ironman-2020/tree/master/static-site-generator/packages/vue-ssr
接續上篇的內容,我們來把 Vuex 跟 vue-router 都加入我們的 SSR 中
我們簡單的建一個 Vuex store :
import Vuex from 'vuex'
import Vue from 'vue'
Vue.use(Vuex)
export default new Vuex.Store({
state: () => ({
message: '',
}),
mutations: {
SET_MESSAGE(state, message) {
state.message = message
},
},
})
然後在 App.vue
的 serverPrefetch 加上抓 API 的程式碼:
import ky from 'ky-universal'
export default {
async serverPrefetch() {
// 這個 API 寫在 server.js 裡
const data = await ky.get('http://localhost:3000/api/foo').json()
this.$store.commit('SET_MESSAGE', data.message)
},
computed: {
message() {
return this.$store.state.message
},
},
}
再來修改 app.js
讓它也回傳 store ,之後也要像這樣把 router 傳出來:
import Vue from 'vue'
import App from './App.vue'
import store from './store'
export function createApp() {
const app = new Vue({
store,
render: (h) => h(App),
})
return { app, store }
}
再來就是重點了,在 entry-server.js
中,我們要把 store 的狀態存進 context
的 state
中:
import { createApp } from './app'
export default (context) => {
const { app, store } = createApp()
// 這個是 vue-server-renderer 的一個 hook ,會在 render 完時呼叫
context.rendered = () => {
// 把 state 存進 context 中
context.state = store.state
}
return app
}
接下來就是 Vue 的魔法了,你可以啟動 server ,看一下產出來的網頁原始碼,你看到了什麼呢?
<script>window.__INITIAL_STATE__={"message":"Hello world"}</script>
vue-server-renderer 已經幫我們把 state 加到產出來的 html 中了,但是如果你打看網頁,應該會發現你看不到這段訊息,因為我們沒有在 Client 端把初始的狀態加回去 Vuex 中,我們在 entry-client.js
中加上:
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
就這樣, Server 的狀態就可以用 Vuex 傳到 Client 端,再來是 vue-router
為了使用 vue-router ,這邊我自己先很簡單的拆了兩個頁面出來,我們直接看 app.js
,這邊把 router 加進:
import Vue from 'vue'
import App from './App.vue'
import store from './store'
import { router } from './router'
export function createApp() {
const app = new Vue({
store,
router,
render: (h) => h(App),
})
return { app, store, router }
}
再來是 entry-server.js
:
import { createApp } from './app'
export default (path, context) => {
// 因為要能等待 router 做完,所以這邊改用 Promise 了
return new Promise((resolve) => {
const { app, store, router } = createApp()
// 把路徑給 router ,讓它去 render 第一個頁面
router.push(path)
// 用 onReady 等待 router 通知完成
router.onReady(() => {
context.rendered = () => {
context.state = store.state
}
// 用 Promise 回傳 app
resolve(app)
})
})
}
而 server.js
則只是因應 entry-server.js
的改變,把 createApp
前加上 await 跟把路徑 /
改成用 /*
而已,就這樣,把 Vue 兩個很重要的部份也加了上去
如果你真的有操作過一次的話,你中間應該有重啟 server 不少次吧,每次要重啟伺服器真的很麻煩, vue-server-renderer 提供了 Bundle Renderer 就是為了解決這個問題,它能夠自動重新載入打包好的 server 端的程式碼,不過因為這個功能似乎還沒支援 webpack 5 ,就大概講一下就好了,用這個要改兩個部份,一個是把 vue-server-renderer/server-plugin
的 webpack plugin 加入 webpack 的設定中,這樣產生出來的就不會是一個 js 檔,而是一個 vue-ssr-server-bundle.json
檔
再來是 createRenderer
改成 createBundleRenderer
:
const renderer = createBundleRenderer('path/to/vue-ssr-server-bundle.json', {template})
而使用時則不用再傳入 app
當參數:
const html = await renderer.renderToString(context)
再來只要搭配 webpack 的 watch mode 就可以自動重新載入了
其實我有嘗試把 vue-server-renderer 的 plugin 修好,不過似乎還是有問題
這是這系列的技術文章部份的最後一篇了